11.4. Conversion Functions

Review

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.

One Function To Rule Them All

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);
};
An image showing the expression 'a+b' written as the function call 'a.operator+(b)' An image showing the expression 'a+5' written as the function call 'a.operator+(5)' An image showing the expression '5+a' written as the function call 'operator+(5,a)'
(a)(b)(c)
A single operator+ function. The important features of this version of the operator+ function are highlighted in yellow: it must be a friend function, and both arguments must be fractions.
  1. The arguments match the operator+ signature exactly, so the function is called directly without any conversion taking place.
  2. The integer 5 must be converted to a fraction; after the conversion, the arguments match the function, which is then called.
  3. The integer 5 must be converted to a fraction; after the conversion, the arguments match the function, which is then called.
Given either a conversion constructor or a conversion operator, the compiler will automatically generate the instructions to perform one conversion operation - it will not chain together multiple conversion operations.

Type Conversion Functions

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.

Fundamental Data Type Conversions

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)
Type casting between fundamental data types.
  1. The final result, stored in the variable "average," is a double, but the operation sum / count is still carried out using integer division: the type conversion takes place after the division but before the assignment
  2. To solve the problem, one or both of the operands must be converted to a double before the division operation takes place. C++ provides two different casting notations as illustrated (Java only supports the first notation)

It 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 Constructor

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)
The fraction conversion constructor. The fraction conversion constructor can be implemented in more than one way, but only one way is needed, and only one way is allowed - that is, the fraction class cannot include both (a) and (b).
  1. A "simple" conversion constructor that converts the integer i into a fraction object
  2. The fraction constructor implemented as a part of the fraction example. With two default arguments, the constructor can be called three different ways, including as a conversion constructor with one argument.
  3. Calls the conversion constructor to convert 5 into the fraction 5/1.
Use a conversion constructor when you "own" the destination type or class (i.e., you are able to edit the class specification of the class to which the conversion constructor is added). It is not necessary to "own" or be able to edit the source type or class. Conversion constructors can also convert from a primitive data type (e.g., an int or a double) to an instance of your class, but not from an instance of your class to a primitive type. For example, let foo and bar be classes; the following conversion constructors convert an instance of bar or an integer into a foo object: You must "own" or be able to edit the specification for the foo class, but ownership of the bar class or of the int type is not necessary.

Conversion Operator

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.

Time Time::add(Time t2)
{
	int	i1 = hours * 3600 + minutes * 60 + seconds;
	int	i2 = t2.hours * 3600 + t2.minutes * 60 + t2.seconds;

	return Time(i1 + i2);
}
The Time add function. The variable i1 represents the total time stored in the implicit or this object, while the variable i2 represents the total time stored in the argument t2.

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.

operator int() { return hours * 3600 + minutes * 60 + seconds; }
The definition of an overloaded conversion operator. Like constructors, conversion operators are always members (i.e., not friends) and do not have an explicit return type (the type is implied by the function name, which, in this example, is int). The conversion operator illustrated here is implemented inside the Time class.

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).

Memberfriend
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)
Calling or using an overloaded conversion operator. A conversion operator may be invoked or called with several different notations as demonstrated by the two versions of 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.
  1. The original casing operator: the parentheses form the casting operator that surrounds the new or destination type and precedes the single operand
  2. Functional notation: the name of the new or destination type forms the function name and the operand is implemented as a parameter enclosed in parentheses
  3. Automatic conversion: beginning with the C++11 standard, type conversions may take place automatically without an explicit call to the casting operator
Use a conversion operator when you do not "own" the destination type or class (i.e., if you are not able to edit the class specification of the class to which the conversion operator is added). This makes conversion operators appropriate for converting from an instance of the defining class to a primitive data type or to an instance of a class the you do not "own." For example, let foo and bar be classes; the following conversion operators are members of the bar class and convert an instance of bar into a foo object or into an int. In this example, you must "own" or be able to edit the bar class, but ownership of the foo class or the int type is not necessary.

 

Conversion Conflicts

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:

  1. operator+(Time, Time): convert 5 to Time with the conversion constructor and call the Time operator+
  2. operator+(int, int): convert t to int with the conversion operator and add two integers with the built in integer "+" operator

Including 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.